package com.guaoran.distributed.io.nio.one.server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * @author : guaoran
 * @Description NIOServer
 * @DESC: 模拟聊天
 * @date :2019/4/18 22:15
 */
/**
 * 网络多客户端聊天室
 * 功能1： 客户端通过Java NIO连接到服务端，支持多客户端的连接
 * 功能2：客户端初次连接时，服务端提示输入昵称，如果昵称已经有人使用，提示重新输入，如果昵称唯一，则登录成功，之后发送消息都需要按照规定格式带着昵称发送消息
 * 功能3：客户端登录后，发送已经设置好的欢迎信息和在线人数给客户端，并且通知其他客户端该客户端上线
 * 功能4：服务器收到已登录客户端输入内容，转发至其他登录客户端。
 *
 * TODO 客户端下线检测
 */
public class NIOServer {
    //相当于自定义协议格式，与客户端协商好
    private final static String USER_CONTENT_SPLIT = "#@#";
    private final static String USER_EXIST = "系统提示：该昵称已存在，请换一个试试";
    private static int port = 8080;
    private Charset charset = Charset.forName("UTF-8");
    // 记录在线人数，以及昵称
    private static HashSet<String> onlineUsers = new HashSet<>();
    private Selector selector;


    public NIOServer(int port) throws IOException {
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        // 绑定ip端口
        socketChannel.bind(new InetSocketAddress(port));
        // 设置非阻塞
        socketChannel.configureBlocking(false);
        // 打开一个选择器
        selector = Selector.open();
        // 将选择器注册到服务端，并打开一个连接的事件
        socketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("服务已启动，监听端口："+port);
    }

    public void listener() throws IOException {
        for(;;){
            int wait = selector.select();
            if(wait ==0) continue;
            // 获得等待的数据
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                // 处理逻辑
                process(key);
                // 处理后删除
                iterator.remove();
            }
        }
    }

    private void process(SelectionKey key) throws IOException {
        // 判断客户端是否已经准备好并且可以实现交互了
        if (key.isAcceptable()){
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            // 获得一个客户端的连接，
            SocketChannel client = serverSocketChannel.accept();
            // 并设置为非阻塞模式
            client.configureBlocking(false);
            // 注册选择器，并设置为可读模式
            // 收到一个连接请求，然后起一个SocketChannel，
            // 并注册到selector上，之后这个连接的数据，就由这个SocketChannel处理
            client.register(selector,SelectionKey.OP_READ);

            // 将此对应的channel设置为准备接受其他客户端请求
            key.interestOps(SelectionKey.OP_ACCEPT);
            client.write(charset.encode("请输入昵称"));
        }
        if (key.isReadable()){
            // 可读事件
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            StringBuffer content = new StringBuffer();

            try {
                while (client.read(buffer)>0){
                    buffer.flip();
                    content.append(charset.decode(buffer));
                }
                //将此对应的channel设置为准备下一次接受数据
                key.interestOps(SelectionKey.OP_READ);
            }catch (IOException e){
                key.cancel();
                if(key.channel() != null){
                    key.channel().close();
                }
            }

            if(content.length()>0){
                String[] arrContent = content.toString().split(USER_CONTENT_SPLIT);
                // 注册用户
                if(arrContent != null && arrContent.length == 1) {
                    String nickName = arrContent[0];
                    if (onlineUsers.contains(nickName)) {
                        client.write(charset.encode(USER_EXIST));
                    } else {
                        onlineUsers.add(nickName);
                        int onlineCount = onlineCount();
                        String message = "欢迎" + nickName + " 进入聊天室！当前在线人数:" + onlineCount;
                        // 广播消息
                        broadCast(message, null);
                    }
                }else if(arrContent != null && arrContent.length >1 ){
                    String nickName = arrContent[0];
                    String message = content.substring(nickName.length() + USER_CONTENT_SPLIT.length());
                    message = nickName + " 说 " + message;
                    if(onlineUsers.contains(nickName)) {
                        //不发给发送此内容的客户端
                        broadCast(message,client);
                    }
                }
            }
        }
    }

    /**
     * 广播群消息
     * @param content
     * @param client
     * @throws IOException
     */
    private void broadCast(String content,SocketChannel client) throws IOException {
        //广播数据到所有的SocketChannel中
        for(SelectionKey key : selector.keys()) {
            Channel targetchannel = key.channel();
            //如果client不为空，不回发给发送此内容的客户端
            if(targetchannel instanceof SocketChannel && targetchannel != client) {
                SocketChannel target = (SocketChannel)targetchannel;
                target.write(charset.encode(content));
            }
        }
    }

    /**
     * 统计在线人数
     * TODO 要是能检测下线，就不用这么统计了
     * @return
     */
    public int onlineCount() {
        int res = 0;
        for(SelectionKey key : selector.keys()){
            Channel target = key.channel();
            if(target instanceof SocketChannel){
                res++;
            }
        }
        return res;
    }


    public static void main(String[] args) throws IOException {
        new NIOServer(port).listener();
    }
}
